#import <appkit/appkit.h>
#import "UniqueKey.h"


static BOOL  _Debug = YES;
static id    _connectionDictionary = nil;


@implementation UniqueKey

/******************************************************************************
* Allow the programmer to specify login information for our separate UniqueKey
* channel.  If no information is supplied, we display our own login panel.
******************************************************************************/
+ setConnectionDictionary:(NSDictionary *)connectionDictionary
{
    if(_connectionDictionary) [_connectionDictionary autorelease];
    _connectionDictionary = [connectionDictionary retain];
    return self;
}
+ connectionDictionary
{
    return [[_connectionDictionary retain] autorelease];
}


/******************************************************************************
* The UniqueKey objects share a database channel that is sure
* to be free to allow immediate reservation of a block of keys.
******************************************************************************/
- initSharedChannel
{
    NSString			*modelName = @"UniqueKey";
    NSString			*path;
    NSString			*_entityName = @"UniqueKey";
    EOModel			*eoModel;
    EOAdaptor			*eoAdaptor;
    EODatabase			*db;
    static EODatabaseContext	*_sharedContext;
    static EODatabaseChannel	*_sharedChannel;
    static EOEntity		*_sharedEntity;
    static BOOL			initFlag = NO;
    
    if(!initFlag) {
	path = [EOModel findPathForModelNamed:modelName];
    	if(!path) {
    		NXRunAlertPanel([NXApp appName],
			"UniqueKey is unable to find %s",NULL,NULL,NULL,[modelName cString]);
		return nil;
	}
	
	eoModel=[[EOModel alloc] initWithContentsOfFile:path];
	if(!eoModel) {
    		NXRunAlertPanel([NXApp appName],
			"UniqueKey is unable to open %s",NULL,NULL,NULL,[path cString]);
		return nil;
   	 }
	 
    	eoAdaptor=[[EOAdaptor adaptorWithModel:eoModel] retain];
   	if(!eoAdaptor) {
    		NXRunAlertPanel([NXApp appName],
			"UniqueKey is unable to create adaptorWithModel:",NULL,NULL,NULL);
		return nil;
   	 }
	 
	if([UniqueKey connectionDictionary]!=nil)
		[eoAdaptor setConnectionDictionary:[UniqueKey connectionDictionary]];
	if(![eoAdaptor hasValidConnectionDictionary])
		[eoAdaptor runLoginPanelAndValidateConnectionDictionary];

   	db=[[EODatabase alloc] initWithAdaptor:eoAdaptor];
   	if(!db) {
    		NXRunAlertPanel([NXApp appName],
			"UniqueKey is unable to initWithAdaptor:",NULL,NULL,NULL);
		return nil;
    	}
    
    	_sharedContext=[[EODatabaseContext alloc] initWithDatabase:db];
    	if(!_sharedContext) {
    		NXRunAlertPanel([NXApp appName],
			"UniqueKey is unable to initWithDatabase:",NULL,NULL,NULL);
		return nil;
    	}
    
    	_sharedChannel=[[EODatabaseChannel alloc] initWithDatabaseContext:_sharedContext];
    	if(!_sharedChannel) {
    		NXRunAlertPanel([NXApp appName],
			"UniqueKey is unable to initWithDatabaseContext:",NULL,NULL,NULL);
		return nil;
   	}
    
    	_sharedEntity=[[eoModel entityNamed:_entityName] retain];
    	if(!_sharedEntity) {
    		NXRunAlertPanel([NXApp appName],
			"UniqueKey is unable to find entityNamed:%s",NULL,NULL,NULL,[_entityName cString]);
		return nil;
    	}

	[[_sharedChannel adaptorChannel] openChannel];
	if(_Debug) [[_sharedChannel adaptorChannel] setDelegate:self];

	initFlag=TRUE;
    }
    
    dbContext        = [_sharedContext retain];
    dbChannel        = [_sharedChannel retain];
    uniqueKeyEntity  = [_sharedEntity retain];
    return self;
}


/******************************************************************************
* Init a UniqueKey object to reserve integer keys in blocks of <count> keys.  Hold
* off reserving any keys until the first call to nextKey.  Each call to nextKey
* returns a unique integer key.  The first call reserves a block of <count> keys.
* When the block has been used, a new block is reserved.
*
* UniqueKey uses a separate table to hold the external entity name and current
* max reserved integer key.  The UniqueKey objects share a database channel that
* is sure to be free to allow immediate reservation of a block of keys.
******************************************************************************/
- initWithEntity:(EOEntity*)entity count:(unsigned int)count
{
    NSString	*qualifierString;
    EOQualifier	*tableNameQualifier;
    NSNumber	*max;

    keyCount   = count;
    nextKey    = 0;
    maxKey     = 0;
    
    if(!entity || ![self initSharedChannel]) return nil;

    // Construct the qualifier for our name
    qualifierString = [NSString stringWithFormat:@"EntityName = '%@'",[entity externalName]];
    
    tableNameQualifier = [[[EOQualifier allocWithZone:[self zone]]
    	initWithEntity:uniqueKeyEntity qualifierFormat:qualifierString] autorelease];

    // Select the object and check for the presence of our name and "MaxKey"
    [dbContext beginTransaction];
    [dbChannel selectObjectsDescribedByQualifier:tableNameQualifier fetchOrder:nil];
    tableMax=[[dbChannel fetchWithZone:[dbChannel zone]] retain];
    [dbChannel cancelFetch];
    [dbContext commitTransaction];
    
    // Was there an entry for our table name?
    if(!tableMax) {
    	NSLog(@"Unique key was unable to fetch EntityName = %@",[entity externalName]);
	return nil;
    }
    
    // Then there should be a value for "MaxKey" for our table
    max=[tableMax objectForKey:@"MaxKey"];
    if(!max) {
	NSLog(@"Unique key method nextKey could not fine MaxKey");
	return nil;
    }
    return self;
}


/******************************************************************************
* Hand out the next unique key from our reserved block.  If the block is exhausted,
* increment the max_id (high-water mark) and attempt to updateObject:.  This
* writes the new max_id back to the table.  Under optimistic locking (the default)
* the update will fail if another client has bumped up the max_id since our last use.
* In that case, we need to refetchObject: and try again.
******************************************************************************/
- (int) nextKey
{
    NSNumber	*max;

    // Hand out our next reserved key.  If we are out of keys, fall through to
    // reserve another block of keyCount keys.
    if(++nextKey<=maxKey) return nextKey;
    
    // Attempt to bump up the count by keyCount.  Fail on optomistic lock if someone
    // else reserved a chunck since our original select (or our last update).
    [dbContext beginTransaction];
    while(1) {
	max    = [tableMax objectForKey:@"MaxKey"];
	maxKey = [max intValue]+keyCount;
	[tableMax setObject:[NSNumber numberWithInt:maxKey] forKey:@"MaxKey"];
    	if([dbChannel updateObject:tableMax]) break;

	[dbContext rollbackTransaction];
	[dbContext beginTransaction];
	[dbChannel refetchObject:tableMax];
    }
    [dbContext commitTransaction];
    
    nextKey=maxKey-keyCount+1;
    return nextKey;
}


/******************************************************************************
* Echo SQL when debug is enabled.
******************************************************************************/
- (EODelegateResponse)adaptorChannel:channel
	willEvaluateExpression:(NSMutableString *)expression
{
	NSLog(expression);
	return EODelegateApproves;
}


@end
